package tx.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.TransactionProxyFactoryBean;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import tx.service.UserService;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * 声明式事务：
 * <pre>
 * 环境搭建：
 * 1、导入相关依赖
 * 		数据源、数据库驱动、Spring-jdbc模块
 * 2、配置数据源、JdbcTemplate（Spring提供的简化数据库操作的工具）操作数据
 * 3、给方法上标注 @Transactional 表示当前方法是一个事务方法；
 * 4、 @EnableTransactionManagement 开启基于注解的事务管理功能；
 *        @EnableXXX
 * 5、配置事务管理器来控制事务;
 *        @Bean
 *        public PlatformTransactionManager transactionManager()
 *
 *
 * 原理：
 * 1）、@EnableTransactionManagement
 * 			利用TransactionManagementConfigurationSelector给容器中会导入组件
 * 			导入两个组件
 * 			AutoProxyRegistrar
 * 			ProxyTransactionManagementConfiguration
 * 2）、AutoProxyRegistrar：
 * 			给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件；
 * 			InfrastructureAdvisorAutoProxyCreator：？
 * 			利用后置处理器机制在对象创建以后，包装对象，返回一个代理对象（增强器），代理对象执行方法利用拦截器链进行调用；
 *
 * 3）、ProxyTransactionManagementConfiguration 做了什么？
 * 			1、给容器中注册事务增强器；
 * 				1）、事务增强器要用事务注解的信息，AnnotationTransactionAttributeSource解析事务注解
 * 				2）、事务拦截器：
 * 					TransactionInterceptor；保存了事务属性信息，事务管理器；
 * 					他是一个 MethodInterceptor；
 * 					在目标方法执行的时候；
 * 						执行拦截器链；
 * 						事务拦截器：
 * 							1）、先获取事务相关的属性
 * 							2）、再获取PlatformTransactionManager，如果事先没有添加指定任何transactionmanger
 * 								最终会从容器中按照类型获取一个PlatformTransactionManager；
 * 							3）、执行目标方法
 * 								如果异常，获取到事务管理器，利用事务管理回滚操作；
 * 								如果正常，利用事务管理器，提交事务
 * </pre>
 * https://blog.csdn.net/f641385712/article/details/89602418
 */
// @ImportResource(value = {"classpath:tx.xml"})
@ComponentScan(value = "tx", excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
})
// @EnableTransactionManagement
@PropertySource(value = {"classpath:dataSource.properties"})
public class RootConfig {

    // 数据源
    @Bean
    public DataSource dataSource(@Value("${db.name}") String name, @Value("${db.password}") String password,
                                 @Value("${db.url}") String url, @Value("${db.driver}") String driver) {
        // MysqlDataSource dataSource = new MysqlDataSource();
        // dataSource.setUser(name);
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUsername(name);
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        // Spring 对 @Configuration 类会特殊处理
        // 给容器中加组件的方法，多次调用都只是从容器中找组件
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
        return new NamedParameterJdbcTemplate(dataSource);
    }

    /**
     * 注册事务管理器在容器中
     * 默认情况下，数据库处于自动提交模式。每一条语句处于一个单独的事务中，在这条语句执行完毕时，如果执行成功则隐式的提交事务；执行失败则隐式的回滚事务
     * 对于正常的事务管理，是一组相关的操作处于一个事务之中，因此必须关闭数据库的自动提交模式。spring 会将底层连接的自动提交特性设置为 false
     * 具体代码在：
     *
     * @see DataSourceTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition)
     * 方法中有一句 con.setAutoCommit(false);
     * 有些数据连接池也提供了关闭事务自动提交的设置
     * 只是 @Transactional(readOnly = true) 这样是不够的，还必须在配置 DataSourceTransactionManager 的时候，
     * 设置 dataSourceTransactionManager.setEnforceReadOnly(true)，最终才会对你的只读事务进行优化。
     * 因为 @Transactional(readOnly = true) 最终会把这个 Connection 设置为只读：con.setReadOnly(true)，
     * 它表示将此连接设置为只读模式，作为驱动程序启用数据库优化的提示。将连接设置为只读模式通知数据库后，数据库会做自己的只读优化。
     * 但是，这对数据库而言不一定这就是 readonly 事务，这点是非常重要的。（因为一个数据库可以有多个连接，一个事务内不可能有多个连接，
     * 因为事务和连接是绑定关系，否则就不叫同一事务了）
     */
    @Bean
    public TransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
        dataSourceTransactionManager.setEnforceReadOnly(true); // 让事务管理器进行只读事务层面上的优化，建议开启
        return dataSourceTransactionManager;
    }

    // ==============================================================================================================

    /**
     * 单独配置每个 Bean 的代理
     */
    // @Bean
    // @Primary
    public TransactionProxyFactoryBean transactionProxyFactoryBean(PlatformTransactionManager transactionManager,
                                                                   UserService userService) {
        TransactionProxyFactoryBean transactionProxyFactoryBean = new TransactionProxyFactoryBean();
        // 设置事务管理器，去管理这个 Bean 的事务关系
        transactionProxyFactoryBean.setTransactionManager(transactionManager);
        // 设置目标对象
        // 这里需要注意：这里设置了目标对象，你的 HelloServiceImpl 就不允许再交给 Spring 管理了，否则会有两个 Bean，不做处理注入会报错
        transactionProxyFactoryBean.setTarget(userService);
        // 设置事务相关的属性：transactionAttributes
        Properties transactionAttributes = new Properties();
        transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
        transactionProxyFactoryBean.setTransactionAttributes(transactionAttributes);

        return transactionProxyFactoryBean;
    }

    // ==============================================================================================================

    /**
     * 配置一个事务模版，用于编程式事务，编程式事务使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager
     * 这里面也可以通过构造函数传入一个 TransactionDefinition 接口的实例。表示事务的一些属性
     */
    // @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager,
                                                   JdbcTemplate jdbcTemplate) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        // 使用
        transactionTemplate.execute(transactionStatus -> {
            // 向数据库插入一条记录
            String sql = "insert into user (name,age) values ('jhxxb',23)";
            jdbcTemplate.update(sql);
            // 做其余的事情，可能抛出异常
            System.out.println(1 / 0);
            return "service hello";
        });
        return transactionTemplate;
    }

    /**
     * 编程式事务 TransactionTemplate，还可以用 JDBC 原生的方式，利用 Connection 或者 SqlSession
     */
    public TransactionManager transactionManager(PlatformTransactionManager transactionManager,
                                                 JdbcTemplate jdbcTemplate) {
        // 构造一个准备使用此事务的定义信息
        DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        transactionDefinition.setReadOnly(false);
        // 隔离级别，-1 表示使用数据库默认级别
        transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        // 根据此事务属性，拿到一个事务实例。注意此处的入参是一个：TransactionDefinition
        TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
        try {
            // =================业务逻辑 start，必须 try，但无需 finally=======================
            // 向数据库插入一条记录
            String sql = "insert into user (name,age) values ('jhxxb',23)";
            jdbcTemplate.update(sql);
            // 做其余的事情，可能抛出异常
            System.out.println(1 / 0);
            // =================业务逻辑 end=======================
            // 提交事务
            transactionManager.commit(transaction);
        } catch (Exception e) {
            // 若发现异常，事务进行回滚
            transactionManager.rollback(transaction);
            throw e;
        }

        return transactionManager;
    }
}
